from http.client import NO_CONTENT
import bcrypt
from jose import JWTError, jwt
from datetime import datetime, timedelta
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from data import Exam, Student, Subject, Teacher, data, flush

SECRET_KEY = "0c89ba1ce79e41f02dd0d95c505075407992f160ef3149a45e68724b36509fc0"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")


class Ok(BaseModel):
    status: int = status.HTTP_200_OK


class Created(BaseModel):
    status: int = status.HTTP_201_CREATED


class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: str | None = None


def get_user(username: str):
    for year in data.teachers:
        for user in data.teachers[year]:
            if user == username:
                return data.teachers[year][user]
    return None


def authenticate_user(username: str, password: str):
    user = get_user(username)
    if not user:
        return False
    if not bcrypt.checkpw(password.encode('utf-8'), user.password):
        return False
    return user


def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    user = get_user(username=token_data.username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(current_user: Teacher = Depends(get_current_user)):
    user = current_user.copy()
    user.password = b''
    return user

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/")
def get_root(current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    return data


@app.get("/notice")
def get_notice():
    return "服务端正在运行中，api版本 " + data.api_version + "\r\n 版权所有，请勿分享"


@app.get("/api-version")
def get_api_version():
    return data.api_version


@app.post("/login", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.name}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/me", response_model=Teacher)
async def read_me(current_user: Teacher = Depends(get_current_active_user)):
    return current_user


@app.get("/exams")
def get_exams(current_user: Teacher = Depends(get_current_active_user)):
    return data.exams


@app.get("/exams/metadata")
def get_exams_metadata(current_user: Teacher = Depends(get_current_active_user)):
    metadata = {}
    for exam_year, exams in data.exams.items():
        metadata[exam_year] = {}
        for exam_name, exam in exams.items():
            metadata[exam_year][exam_name] = {
                'exam_year': exam_year,
                'time': exam.time,
                'status': exam.status.value,
                'subjects': [subject for subject in exam.subjects.keys()],
            }
    return metadata


@app.get("/exams/{year}")
def get_exams_with_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="year not found")
    return data.exams[year]


# 添加一届考试
@app.post("/exams/{year}", status_code=status.HTTP_201_CREATED)
def post_exam_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year in data.exams:
        return HTTPException(status_code=status.HTTP_409_CONFLICT, detail="exam year already exists")
    data.exams[year] = {}
    flush()
    return Created()


# 删除一届考试
@app.delete("/exams/{year}")
def delete_exam_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    del data.exams[year]
    return data.exams


@app.get("/exams/{year}/{name}")
def get_exams_with_year_and_name(year: int, name: str, current_user: Teacher = Depends(get_current_active_user)):
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    return data.exams[year][name]


@app.post("/exams/{year}/{name}", status_code=status.HTTP_201_CREATED)
def post_exams_with_year_and_name(year: int, name: str, exam: Exam, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name in data.exams[year]:
        return HTTPException(status_code=status.HTTP_409_CONFLICT, detail="exam name already exists")
    data.exams[year][name] = Exam(
        year=year, name=name, time=exam.time, status=exam.status, subjects=exam.subjects)
    flush()
    return Created()


# 只能修改考试元数据
@app.put("/exams/{year}/{name}")
def put_exams_with_year_and_name(year: int, name: str, exam: Exam, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    subjects = {}
    for subject_name, scores in exam.subjects.items():
        if subject_name in data.exams[year][name].subjects:
            subjects[subject_name] = data.exams[year][name].subjects[subject_name]
        else:
            subjects[subject_name] = {}
    data.exams[year][name] = Exam(**exam.dict())
    flush()
    return Ok()


@app.delete("/exams/{year}/{name}", status_code=status.HTTP_204_NO_CONTENT)
def delete_exams_with_year_and_name(year: int, name: str, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    del data.exams[year][name]
    return


@app.get("/exams/{year}/{name}/{student_name}")
def get_student_scores(year: int, name: str, student_name: str):
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    student_scores = {}
    for subject_name, scores in data.exams[year][name].subjects.items():
        if student_name in scores:
            student_scores[subject_name] = scores[student_name]
    if student_scores == {}:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student not found")
    return student_scores


'''
@app.get("/exams/{year}/{name}/{subject}")
def get_exams_subject(year: int, name: str, subject: Subject, current_user: Teacher = Depends(get_current_active_user)):
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    if subject not in data.exams[year][name].subjects:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam subject not found")
    return data.exams[year][name].subjects[subject]
'''


@app.get("/exams/{year}/{name}/{subject}/{student_name}")
def get_exams_with_subject_and_student_name(year: int, name: str, subject: Subject, student_name: str, current_user: Teacher = Depends(get_current_active_user)):
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    if subject not in data.exams[year][name].subjects:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam subject not found")
    if student_name not in data.exams[year][name].subjects[subject].students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam student not found")
    return data.exams[year][name].subjects[subject][student_name]


def student_is_in_class(year: int, student_name: str, classes_name: list[str]):
    for class_name in classes_name:
        if student_name in data.students[year][class_name]:
            return True


@app.post("/exams/{year}/{name}/{subject}/{student_name}", status_code=status.HTTP_201_CREATED)
def post_exams_with_subject_and_student_name(year: int, name: str, subject: Subject, student_name: str, scores: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0" and (current_user.year != year and student_is_in_class(year, student_name, current_user.classes)):
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    if subject not in data.exams[year][name].subjects:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam subject not found")
    data.exams[year][name].subjects[subject][student_name] = scores
    flush()
    return Created()


@app.put("/exams/{year}/{name}/{subject}/{student_name}")
def put_exams_with_subject_and_student_name(year: int, name: str, subject: Subject, student_name: str, scores: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0" and (current_user.year != year and student_is_in_class(year, student_name, current_user.classes)):
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    if subject not in data.exams[year][name].subjects:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam subject not found")
    data.exams[year][name].subjects[subject][student_name] = scores
    flush()
    return Ok()


@app.delete("/exams/{year}/{name}/{subject}/{student_name}", status_code=status.HTTP_204_NO_CONTENT)
def delete_exams_with_subject_and_student_name(year: int, name: str, subject: Subject, student_name: str, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0" and (current_user.year != year and student_is_in_class(year, student_name, current_user.classes)):
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.exams:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam year not found")
    if name not in data.exams[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam name not found")
    if subject not in data.exams[year][name].subjects:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam subject not found")
    if student_name not in data.exams[year][name].subjects[subject]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="exam student not found")
    del data.exams[year][name].subjects[subject][student_name]
    return


@app.get("/students")
def get_students(current_user: Teacher = Depends(get_current_active_user)):
    return data.students


@app.get("/students/{year}")
def get_classes_with_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    return data.students[year]


# 添加一届学生
@app.post("/students/{year}", status_code=status.HTTP_201_CREATED)
def post_students_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year in data.students:
        return HTTPException(status_code=status.HTTP_409_CONFLICT, detail="student year already exists")
    data.students[year] = {}
    flush()
    return Created()


# 删除一届学生
@app.delete("/students/{year}", status_code=status.HTTP_204_NO_CONTENT)
def delete_students_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    del data.students[year]
    return


@app.get("/students/{year}/{class_name}")
def get_class_with_year_and_class_name(year: int, class_name: str, current_user: Teacher = Depends(get_current_active_user)):
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    if class_name not in data.students[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student class not found")
    return data.students[year][class_name]


@app.post("/students/{year}/{class_name}", status_code=status.HTTP_201_CREATED)
def post_class_with_year_and_class_name(year: int, class_name: str, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    if class_name in data.students[year]:
        return HTTPException(status_code=status.HTTP_409_CONFLICT, detail="student class already exists")
    data.students[year][class_name] = {}
    flush()
    return Created()


@app.delete("/students/{year}/{class_name}", status_code=status.HTTP_204_NO_CONTENT)
def delete_class_with_year_and_class_name(year: int, class_name: str, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    if class_name not in data.students[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student class not found")
    del data.students[year][class_name]
    return


@app.get("/students/{year}/{class_name}/{name}")
def get_students_with_year_and_class_name_and_student_name(year: int, class_name: str, name: str, current_user: Teacher = Depends(get_current_active_user)):
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    if class_name not in data.students[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student class not found")
    if name not in data.students[year][class_name]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student name not found")
    return data.students[year][class_name][name]


@app.post("/students/{year}/{class_name}/{name}", status_code=status.HTTP_201_CREATED)
def post_students_with_year_and_class_name_and_student_name(year: int, class_name: str, name: str, student: Student, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    if class_name not in data.students[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student class not found")
    data.students[year][class_name][name] = Student(**student.dict())
    flush()
    return Created()


@app.put("/students/{year}/{class_name}/{name}")
def put_students_with_year_and_class_name_and_student_name(year: int, class_name: str, name: str, student: Student, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    if class_name not in data.students[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student class not found")
    if name not in data.students[year][class_name]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student name not found")
    data.students[year][class_name][name] = student
    flush()
    return Ok()


@app.delete("/students/{year}/{class_name}/{name}", status_code=status.HTTP_204_NO_CONTENT)
def delete_students_with_year_and_class_name_and_student_name(year: int, class_name: str, name: str, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.students:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student year not found")
    if class_name not in data.students[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student class not found")
    if name not in data.students[year][class_name]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="student name not found")
    del data.students[year][class_name][name]
    return


@app.get("/teachers")
def get_teachers(current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    return data.teachers


@app.get("/teachers/{year}")
def get_teachers_with_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.teachers:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher year not found")
    return data.teachers[year]


# 添加一届老师
@app.post("/teachers/{year}", status_code=status.HTTP_201_CREATED)
def post_teachers_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year in data.teachers:
        return HTTPException(status_code=status.HTTP_409_CONFLICT, detail="teacher year already exists")
    data.teachers[year] = {}
    flush()
    return Created()


# 删除一届老师
@app.delete("/teachers/{year}", status_code=status.HTTP_204_NO_CONTENT)
def delete_teachers_year(year: int, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.teachers:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher year not found")
    del data.teachers[year]
    return


@app.get("/teachers/{year}/{name}")
def get_teachers_with_year_and_name(year: int, name: str, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.teachers:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher year not found")
    if name not in data.teachers[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher name not found")
    return data.teachers[year][name]


@app.post("/teachers/{year}/{name}", status_code=status.HTTP_201_CREATED)
def post_teachers_with_year_and_name(year: int, name: str, teacher: Teacher, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.teachers:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher year not found")
    data.teachers[year][name] = teacher
    flush()
    return Created()


@app.put("/teachers/{year}/{name}")
def put_teachers_with_year_and_name(year: int, name: str, teacher: Teacher, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.teachers:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher year not found")
    if name not in data.teachers[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher name not found")
    data.teachers[year][name] = teacher
    flush()
    return Ok()


@app.delete("/teachers/{year}/{name}", status_code=status.HTTP_204_NO_CONTENT)
def delete_teachers_with_year_and_name(year: int, name: str, current_user: Teacher = Depends(get_current_active_user)):
    if current_user.id_number != "0":
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="no access")
    if year not in data.teachers:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher year not found")
    if name not in data.teachers[year]:
        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="teacher name not found")
    del data.teachers[year][name]
    return
